﻿//------------------------------------------------------------------------------
// <copyright file="LoopbackConnection.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
// A loopback connection that pipes data across TCP connections.
// </summary>
//------------------------------------------------------------------------------

namespace Microsoft.Internal.GamesTest.Rpc.Tests.RemoteLoopback
{
    // Standard namespaces...
    using System;
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;

    // User-defined namespaces...
    using Microsoft.Internal.GamesTest.Rpc.Tests.Utilities;

    /// <summary>
    /// A loopback connection that pipes data across TCP connections.
    /// </summary>
    public class LoopbackConnection
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="LoopbackConnection" /> class.
        /// </summary>
        /// <param name="inboundClient">The TCP client of the inbound connection.</param>
        /// <param name="server">The loopback server hosting the connection.</param>
        /// <param name="id">The ID of the connection.</param>
        public LoopbackConnection(TcpClient inboundClient, LoopbackServer server, uint id)
        {
            // Initialize...
            this.Id = id;
            this.EstablishedTime = DateTime.Now;
            this.Server = server;
            this.InboundClient = new LoopbackClient(inboundClient);
            this.ShowMessage("Connection received from {0}.", this.InboundClient);

            // Initialize the outbound client...
            try
            {
                if (string.IsNullOrEmpty(Settings.Default.OutboundTarget) ||
                    string.Compare(Settings.Default.OutboundTarget, "loopback", true) == 0)
                {
                    this.CreateRelayConnection(this.InboundClient);
                }
                else
                {
                    this.CreateRelayConnection(Settings.Default.OutboundTarget);
                }
            }
            catch (Exception)
            {
                Console.WriteLine("Unable to connect to the relay client.");

                this.InboundClient.Close();
                return;
            }

            // Start a thread for relaying data received from the inbound client...
            this.InboundThread = new Thread(new ThreadStart(this.HandleInboundCommunication));
            this.InboundThread.Start();

            // Start a thread for relaying data received from the outbound client...
            this.OutboundThread = new Thread(new ThreadStart(this.HandleOutboundCommunication));
            this.OutboundThread.Start();
        }

        /// <summary>
        /// Gets the ID of the connection.
        /// </summary>
        /// <value>The ID of the connection.</value>
        public uint Id { get; private set; }

        /// <summary>
        /// Gets the date/time when this connection was established.
        /// </summary>
        /// <value>The date time established.</value>
        public DateTime EstablishedTime { get; private set; }

        /// <summary>
        /// Gets the TCP client that initiated communication.
        /// </summary>
        /// <value>The TCP client that initiated communication.</value>
        public LoopbackClient InboundClient { get; private set; }

        /// <summary>
        /// Gets the thread that handles data received from the inbound connection.
        /// </summary>
        /// <value>The thread that handles data received from the inbound connection.</value>
        public Thread InboundThread { get; private set; }

        /// <summary>
        /// Gets the TCP client the communication is being relayed to.
        /// </summary>
        /// <value>The TCP client the communication is being relayed to.</value>
        public LoopbackClient OutboundClient { get; private set; }

        /// <summary>
        /// Gets the thread that handles data received from the outbound connection.
        /// </summary>
        /// <value>The thread that handles data received from the outbound connection.</value>
        public Thread OutboundThread { get; private set; }

        /// <summary>
        /// Gets or sets the loopback server hosting the connection.
        /// </summary>
        /// <value>The loopback server hosting the connection.</value>
        private LoopbackServer Server { get; set; }

        /// <summary>
        /// Converts the loopback client to a string.
        /// </summary>
        /// <returns>A string that represents the loopback client.</returns>
        public override string ToString()
        {
            return string.Format("{0} ({1})", this.Id, this.InboundClient.ToShortString());
        }

        /// <summary>
        /// Create the relay connection.
        /// </summary>
        /// <param name="host">The host to connect to.</param>
        private void CreateRelayConnection(LoopbackClient host)
        {
            this.ShowMessage(" - Creating a relay connection to {0}...", host);
            TcpClient outbound = new TcpClient();
            outbound.Connect(host.IPAddress, Settings.Default.OutboundPort);
            this.OutboundClient = new LoopbackClient(outbound, host.HostName);
            this.ShowMessage(" - Connection established.");
        }

        /// <summary>
        /// Create the relay connection.
        /// </summary>
        /// <param name="host">The name of the host to connect to.</param>
        private void CreateRelayConnection(string host)
        {
            this.ShowMessage(" - Creating a relay connection to {0}...", host);
            TcpClient outbound = new TcpClient();
            outbound.Connect(host, Settings.Default.OutboundPort);
            this.OutboundClient = new LoopbackClient(outbound, host);
            this.ShowMessage(" - Connection established.");
        }

        /// <summary>
        /// Displays a message to the user.
        /// </summary>
        /// <param name="format">A composite format <c>string</c>.</param>
        /// <param name="args">An <c>System.Object</c> array containing zero or more objects to format.</param>
        /// <returns>The formatted message tagged with the date, time and ID of the connection.</returns>
        private string FormatMessage(string format, params object[] args)
        {
            // Variables...
            string message = string.Empty;

            // Format the message...
            if (args == null || args.Length < 1)
            {
                message = format;
            }
            else
            {
                message = string.Format(format, args);
            }

            // Add the additional information...
            return string.Format("[{0:D02}]: {1}", this.Id, message);
        }

        /// <summary>
        /// Handle the communication between two clients.
        /// </summary>
        /// <param name="readClient">The client to read data from.</param>
        /// <param name="writeClient">The client to relay data to.</param>
        /// <param name="type">The type of communication being handled.</param>
        private void HandleCommunication(LoopbackClient readClient, LoopbackClient writeClient, string type)
        {
            try
            {
                // Initialize...
                int bytesRead = 0;
                byte[] data = new byte[Settings.Default.DataBufferSize];
                NetworkStream readStream = readClient.Client.GetStream();
                NetworkStream writeStream = writeClient.Client.GetStream();

                while (true)
                {
                    // Make sure the client is still connected...
                    if (!readClient.Client.Connected)
                    {
                        this.ShowMessage("The {0} client {1} disconnected.", type, readClient);
                        break;
                    }

                    // Wait for data...
                    if (readClient.Client.Available <= 0)
                    {
                        Thread.Sleep(Settings.Default.PendingReadCheckInterval);
                        continue;
                    }

                    // Read the data from the connection...
                    bytesRead = readStream.Read(data, 0, Settings.Default.DataBufferSize);

                    // See if the client has disconnected from the server...
                    if (bytesRead == 0)
                    {
                        this.ShowMessage("The {0} client {1} disconnected.", type, readClient);
                        break;
                    }

                    // See if the target is still available...
                    if (!writeClient.Client.Connected)
                    {
                        break;
                    }

                    // Relay the data received on to the target...
                    try
                    {
                        writeStream.Write(data, 0, bytesRead);
                        writeStream.Flush();
                    }
                    catch (IOException)
                    {
                        this.ShowMessage("The {0} client failed to relay the message to {1}.", type, writeClient);
                    }
                }
            }
            catch (Exception e)
            {
                this.ShowError(e, "Unexpected error reading from the {0} client, {1}.  Closing the connection...", type, this.InboundClient.ToString());
            }
            finally
            {
                // Close the clients, but only let one of the threads do it...
                lock (this.Server.ActiveConnectionsLock)
                {
                    // Checks to make sure that the connection still exists in the list before removing it
                    if (this.Server.ActiveConnections.Contains(this))
                    {
                        readClient.Close();
                        writeClient.Close();
                        this.Server.RemoveConnection(this);
                    }
                }
            }
        }

        /// <summary>
        /// Handle the communication from the inbound client to the outbound client.
        /// </summary>
        private void HandleInboundCommunication()
        {
            this.HandleCommunication(this.InboundClient, this.OutboundClient, "inbound");
        }

        /// <summary>
        /// Handle the communication from the outbound client to the inbound client.
        /// </summary>
        private void HandleOutboundCommunication()
        {
            this.HandleCommunication(this.OutboundClient, this.InboundClient, "outbound");
        }

        /// <summary>
        /// Displays an error message to the user.
        /// </summary>
        /// <param name="e">The exception to display.</param>
        /// <param name="format">A composite format <c>string</c>.</param>
        /// <param name="args">An <c>System.Object</c> array containing zero or more objects to format.</param>
        private void ShowError(Exception e, string format, params object[] args)
        {
            Console.WriteLine(this.FormatMessage(format, args), e);
        }

        /// <summary>
        /// Displays a message to the user.
        /// </summary>
        /// <param name="format">A composite format <c>string</c>.</param>
        /// <param name="args">An <c>System.Object</c> array containing zero or more objects to format.</param>
        private void ShowMessage(string format, params object[] args)
        {
            Console.WriteLine(this.FormatMessage(format, args));
        }

        /// <summary>
        /// Displays an warning message to the user.
        /// </summary>
        /// <param name="e">The exception to display.</param>
        /// <param name="format">A composite format <c>string</c>.</param>
        /// <param name="args">An <c>System.Object</c> array containing zero or more objects to format.</param>
        private void ShowWarning(Exception e, string format, params object[] args)
        {
            Console.WriteLine(this.FormatMessage(format, args), e);
        }
    }
}
